Les pièges du langage Python **************************** Le langage Python dispose d'une syntaxe extrêmement simple qui avec le temps devenu une de ses principales forces. Rappelons qu'historiquement ce langage avait été conçu pour enseigner la programmation aux enfants ! Mais derrière cette syntaxe enfantine se cache, comme tous les autres langages, des pièges loin d'être évidents. Nous présentons dans ce chapitre les pièges les plus courants faisant généralement perdre pas mal de temps aux développeurs. Conflit entre variables globales et locales =========================================== Dans le corps d'une fonction, si aucune variable locale ne porte le nom recherché, alors on recherche une variable globale portant ce nom. Voici un exemple : .. code-block:: py a = 5 def fnt1(): a = 6 # définition d'une variable locale valant 6 masquant la variable globale print(a) # >> 6 def fnt2(): print(a) # absence de variable locale, on consulte la variable globale La syntaxe : *a = ...* dans une fonction crée, par convention, une variable locale. Si une variable globale porte le même nom, elle est donc masquée par cette nouvelle variable locale. Cependant, si vous cherchez à modifier une variable globale en écrivant *a=6*, vous n'allez pas y arriver de cette façon. Pour modifier une variable globale depuis l'intérieur d'une fonction, il faut utiliser le mot clef *global* : .. panels:: :column: col-lg-10 p-2 Pour pouvoir accéder à une variable globale en écriture à l'intérieur d'une fonction, il faut en début de fonction utiliser le mot clef **global** suivi du nom de la variable. Voici un exemple : .. code-block:: py a = 4 def fnt3(): global a # relie le nom "a" à la variable globale nommée "a" a = 6 # la variable globale vaut maintenant 6 fnt3() print(a) # >> 6 Passage ======= Le tout objet ------------- Aussi étrange que cela puisse paraître, en Python tout est objet. Ainsi la notion de type de base (int, float) que l'on rencontre dans les autres langages comme Java/C/C++ n'existe pas. Un moyen de s'en rendre compte est de travailler sur de grands entiers : .. code-block:: a = 10 for i in range(5): a = a * a print(a,type(a)) >> 100 >> 10000 >> 100000000 >> 10000000000000000 >> 100000000000000000000000000000000 Si nous avions travaillé en C/C++/Java, pour stocker ces résultats, il aurait fallu utiliser un int, puis un __int64 et finalement un BigInteger. Ici, tout est transparent à notre niveau, la classe int Python s'est chargée de cela à notre place. .. warning:: Les numériques sont immutables en Python, on ne peut modifier leur valeur. Dans cette logique, l'opérateur ++ et -- n'existent pas en Python. Cependant il est possible d'écrire *a = a + 4* car on associe la variable *a* à un nouvel objet, on ne modifie pas son contenu ! Passage par affectation ----------------------- Dans le langage Python, lors de l'appel d'une fonction, **les arguments sont passés par affectation**. Ainsi, pour chaque paramètre est créé une variable locale faisant référence à l'objet passé en argument. Ce mode de passage correspond au passage par adresse en C/C++ (par pointeur) et au passage par référence en Java/C#. Il ne s'agit pas du passage par référence du C++ car l'argument et le paramètre, en Python, sont deux variables distinctes. Pour tester cela, nous allons utiliser la fonction **id()** qui retourne le numéro d'identification unique de chaque objet. En conclusion : .. panels:: :column: col-lg-10 p-2 Une affectation effectuée sur un paramètre de la fonction ne modifie pas l'objet initial passé en argument. Cas des valeurs numériques ^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: py a = 6 def fnt(b): # la variable T désigne la liste [1,2,3] print(id(a),id(b)) # 914 914, à ce niveau les variables a et b désignent le même objet print(a,b) # 6 6 b = 7 # b est associé à un nouvel objet, cette affectation ne modifie pas la variable originale a print(a,b) # 6 7 print(id(a),id(b)) # 914 2298 à ce niveau les variables a et b désignent des objets différents fnt(a) .. warning:: Où est le piège ? Si l'on ignore cette logique, on est tenté de penser, par analogie avec les autres langages, que Python effectue un passage par valeur sur les types numériques, ce qui n'est pas le cas... Cas des listes ^^^^^^^^^^^^^^ .. code-block:: py L = [ 1 , 2 , 3 ] def fnt(T): # la variable T désigne la liste [1, 2, 3] T[0] = 7 # cette affectation modifie une valeur dans la liste originale print(id(L),id(T)) # 59912 59912 => L et T désignent la même liste T = [1,2] # T est une variable locale, on peut l'affecter à une autre liste comme toute variable ! # cette affectation ne modifie en rien la liste L print(id(L),id(T)) # 59912 186 => L et T désignent des listes différentes fnt(L) print(L) # [7, 2, 3] l'affectation T[0] = 7 a modifié L mais pas l'affectation T = [1,2] Ici, si l'on oublie le fonctionnement interne de Python, on peut ne plus rien comprendre à ce qu'il se passe car on observe deux comportements différents pour l'affectation : * L'écriture T[0] = 7 modifie un élément de la liste *L*, car l'affectation se produit sur le premier élément de la liste * L'écriture T = [1,2] ne change pas la liste *L* car ici on associe la variable T à une autre liste, on ne change pas le contenu de L Modifier un entier passé en argument ==================================== Nous venons de voir qu'il n'est pas possible de modifier un entier passé en paramètre depuis l'intérieur d'une fonction. Pour effectuer cela, on peut utiliser le mécanisme de retour d'une fonction : .. code-block:: py x, y = 1, 2 def fnt1(a, b): a += 1 # a et b sont des variables locales b += 1 print(x,y) # 1 2 car modifier a et b n'a pas changé x et y return a,b # retour des résultats x, y = fnt1(x, y) # récupération des résultats et affection de x et y dans la foulée print(x,y) # 2 3 L'autre astuce consiste à stocker les valeurs dans une liste qui permet la modification de ses valeurs à l'intérieur de la fonction : .. code-block:: py X = [1,2] def fnt2(L): L[0] += 1 # La liste L correspond à la liste X L[1] += 1 print(X) # 2 3 => les valeurs dans la liste originale ont été modifiées fnt2(X) print(X) >> 2 3 Clonage ======= Problématique ------------- Nous avons vu que si *L* est une liste, alors l'écriture *T=L* crée une variable *T* désignant la liste initiale *L*, il n'y a pas création d'une deuxième liste. Si vous ne comprenez pas ce mécanisme, vous ne comprendrez pas pourquoi en modifiant les valeurs dans *T*, la liste *L* est aussi modifiée ! Voici un exemple : .. code-block:: py L = [1, 2, 3] T = L T[0] += 1 # la modification sur T se fait sur la liste originale L print(L) >> [2, 2, 3] Effectuer une copie superficielle --------------------------------- Pour cela, il faut utiliser la fonction **copy()** pour effectuer une duplication : .. code-block:: py L = [1, 2, 3] T = L.copy() T[0] += 1 print(T) >> [2, 2, 3] print(L) >> [1, 2, 3] Mais attention, il s'agit d'une copie superficielle, voir le code ci-dessous, les éléments à l'intérieur de la liste ne sont pas dupliqués ! .. code-block:: py def copy(L): T = [] for e in L: T.append(e) return T Si vous avez des immutables dans votre liste, des numériques par exemples, cela ne posera pas problème. Mais dans le cas général, la fonction copy() sera insuffisante : .. code-block:: py L = [ [1,2], [3,4] ] T = L.copy() T.append([5,6]) print(L) >> [ [1,2], [3,4] ] print(T) >> [ [1,2], [3,4] , [5,6] ] # T et L sont deux listes indépendantes, mais les listes qu'elles contiennent sont liées : L[0][0] = 9 L[1][1] = 8 print(L) >> [ [9,2], [3,8] ] print(T) >> [ [9,2], [3,8] , [5,6] ] # oups c'est l'accident, en modifiant L on a modifié T !! Effectuer une copie profonde ---------------------------- Pour dupliquer un objet en construisant une copie profonde de manière récursive, il faut utiliser la fonction **copy.deepcopy()** : .. code-block:: py import copy L = [ [1,2], [3,4] ] T = copy.deepcopy(L) L[0][0] = 9 L[1][1] = 8 print(L) >> [[9, 2], [3, 8]] print(T) >> [[1,2], [3,4]] Les listes présentes dans la liste T sont indépendantes de celles de la liste L .. quiz:: quizzpython33 :title: Questionnaire Pour chaque affirmation, indiquez si elle est vraie ou fausse. .. csv-table:: :widths: 40, 10 :delim: ! Si on modifie les éléments d'une liste, l'identificateur de la liste change (id) ! :quiz:`{"type":"TF","answer":"F"}` Le mot clef global permet d'avoir un accès en écriture vers une variable globale ! :quiz:`{"type":"TF","answer":"T"}` Pour effectuer une copie profonde, il faut utiliser la fonction *copy()* ! :quiz:`{"type":"TF","answer":"F"}` Une affectation effectuée sur un paramètre de fonction modifie l'objet passé en argument ! :quiz:`{"type":"TF","answer":"F"}` Les listes sont de type mutable ! :quiz:`{"type":"TF","answer":"T"}` Les tuples sont de type mutable ! :quiz:`{"type":"TF","answer":"F"}` Les int sont de type mutable ! :quiz:`{"type":"TF","answer":"F"}` L'opérateur ++ n'existe pas en Python ! :quiz:`{"type":"TF","answer":"T"}`